《Android 安全(二)》Smali语法基础

Github原文

Smali

smali/baksmali是Android的Java VM实现dalvik使用的dex格式的汇编程序/反汇编程序。 语法松散地基于Jasmin/ dedexer的语法,并支持dex格式的全部功能(注释,调试信息,行信息等)

BuildProcedure(构建程序)

依赖

这可能是一份不完整的清单。如果您遇到遗漏的内容,请随时发表评论,我会将其添加

  • jdk(8)
  • git

其他所有内容都应该通过gradle / gradlew下载

构建

1
2
3
git clone https://github.com/JesusFreke/smali.git
cd smali
./gradlew build

(windows上,使用gradlew.bat 代替./gradlew) 生成的jar包位置

1
2
3
smali/build/libs/smali-<version>.jar

baksmali/build/libs/baksmali-<version>.jar

除此之外,通过

1
./gradlew proguard

可以构建出体积更小混淆后的jar文件

1
2
3
smali/build/libs/smali-<version>-small.jar

baksmali/build/libs/baksmali-<version>-small.jar

DeodexInstructions(Deodex说明)

说明

从v2.1.0开始,baksmali支持反编译ART oat文件。 支持的最低oat版本为56,也就是Android 6.0 / Marshmallow版本中的oat文件版本。 由于与字段排序有关的一些潜在问题,暂不支持M版本之前的oat文件反编译(例如Lollipop)。

你可以在oat文件上运行baksmali以获取其中所有odex文件的列表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
> adb pull /system/framework/arm/boot.oat boot.oat
> baksmali boot.oat
boot.oat contains multiple dex files. You must specify which one to disassemble with the -e option
Valid entries include:
/system/framework/core-libart.jar
/system/framework/conscrypt.jar
/system/framework/okhttp.jar
/system/framework/core-junit.jar
/system/framework/bouncycastle.jar
/system/framework/ext.jar
/system/framework/framework.jar
/system/framework/framework.jar:classes2.dex
/system/framework/telephony-common.jar
/system/framework/voip-common.jar
/system/framework/ims-common.jar
/system/framework/mms-common.jar
/system/framework/android.policy.jar
/system/framework/apache-xml.jar

通过如下操作我们可以反编译boot.oat中的framework jar文件

1
2
> adb pull /system/framework/arm/boot.oat /tmp/framework/boot.oat
> baksmali -x -c boot.oat -d /tmp/framework -e /system/framework/framework.jar /tmp/framework/boot.oat -o framework

或者通过如下方式反编译一个应用程序oat

1
2
3
> adb pull /system/framework/arm/boot.oat /tmp/framework/boot.oat
> adb pull /system/app/Calculator/oat/arm/Calculator.odex Calculator.odex
> baksmali -x -c boot.oat -d /tmp/framework Calculator.odex -o Calculator

注意,在反编译oat文件时不需要-a选项来指定API Level,baksmali会自动使用oat文件中的API Level。

从v1.4.0开始,deodexing过程已经大大简化了,不再需要通过-c选项指定其他类路径。

Deodexing现在很简单

1
baksmali -a <api_level> -x <odex_file> -d <framework_dir>

odex文件常识

简而言之,odex文件就是classes.dex文件的优化版本,针对特定设备的优化。 值得一提的是,odex文件依赖于生成时加载的每个“BOOTCLASSPATH”文件。 odex文件仅在与这些确切的BOOTCLASSPATH文件一起使用时才有效。 dalvik通过存储odex文件所依赖的每个文件的校验和来强制执行此操作,并确保在加载odex文件时每个文件的校验和匹配。

除了加载的主apk / jar之外,BOOTCLASSPATH只是可以加载类的jars / apk列表而已。 一般android系统的基础BOOTCLASSPATH中包含5个jar包 - core.jar,ext.jar,framework.jar,android.policy.jar和services.jar。 这些都可以在/ system / framework中找到。 但是,有些apks依赖于额外的jar或apks文件。 例如,对于使用谷歌地图的应用程序,com.google.android.maps.jar将附加到该应用程序的apk的BOOTCLASSPATH中。

下面这几个由于odex的依赖性造成的问题有时会让事情变得比较麻烦。 首先 - 你不能从一个系统镜像中获取apk + odex文件并在另一个系统映像上运行它(除非其他系统映像使用完全相同的framework文件)。 另一个问题是,如果对任何BOOTCLASSPATH文件进行任何更改,它将使依赖于该文件的每个odex无效 - 基本上每个设备上的apk / jar都会失效。

Deodexing(反编译Odex)

以下示例默认你使用下载页面上提供的baksmali wrapper脚本来调用baksmali。 如果你是直接调用jar,可以将“baksmali”替换为“java -jar baksmali.jar”

deodexing的两个主要选项是-x和-d。 -x是告诉baksmali你想要deodex的对象,-d告诉baksmali从哪里加载依赖项。
依赖内容可以是odex文件的形式,也可以是包含classes.dex文件的jar / apk。两者皆可。

例如,你想从ICS镜像中反编译Calculator.odex文件并且所有的framework odex文件和jar包都放在一个名为framework的文件夹中,你就可以通过如下方式操作:

1
baksmali -a 15 -x Calculator.odex -d framework -o Calculator

反编译Calculator.odex,并将生成的deodexed smali文件放入名为Calculator的目录中。

Troubleshooting(问题解决)

Heap Space

你可能遇到的一个问题是它是否已用完堆内存。 该错误可能类似于“java.lang.OutOfMemoryError:Java heap space”。 要解决此问题,可以使用-Xmx参数来增加堆大小。 尝试将其设置为512m。 当然,如果需要,你可以进一步增加它。 如果你使用wrapper script来调用baksmali,则可以使用-JXmx。 例如:

1
baksmali -JXmx512m -x blah.odex

或者通过如下方式直接调用jar包

1
java -Xmx512m -jar baksmali.jar -x blah.odex

Registers(寄存器)

介绍

在dalvik字节码中,寄存器是32位,并且可以保存任何类型的值。 2个寄存器用于保存64位类型(Long和Double)。

置顶方法中寄存器数量

有两种方法可以指定方法中有多少可用寄存器。 .registers指令指定方法中寄存器的总数,而.locals指令指定方法中非参数寄存器的数量。 寄存器总数包括保存方法参数所需的寄存器。

方法参数如何传递至方法

调用方法时,方法的参数将会被放置到倒数的n个寄存器中。如果某个方法有2个参数,5个寄存器(v0-v4),此时参数会被放置到最后两个寄存器中,也就是v3和v4。
对于非static方法,第一个参数永远都是调用该方法的对象。
比如,非static方法LMyObject;->callMe(II)V.,这个方法有2个int型参数,但是还存在一个隐型的 LMyObject; 参数在两个整型参数之前,所以这个方法有3个参数。

假设通过.registers 5 指令或者.local 2指令(i.e. 2个local寄存器和3个参数寄存器)来指定这个方法有5个寄存器(v0 - v4),当方法被调用时,调用此方法的对象(也就是 this 引用)将会被放置到v2寄存器中,而第一个整型参数将会被放置到v3寄存器中,第二个整型参数会被放置到v4寄存器中。

对于static方法,只是少了一个隐型参数,this而已。

Register names(寄存器名称)

对于寄存器,有两种命名方案:普通的v开头命名方案和针对参数寄存器的p开头命名方案。我们继续看上面那个有3个参数,5个寄存器的方法,下面这个表格分别用两种命名方式来表示寄存器。

Local Param 数量
v0 第一个local寄存器
v1 第二个local寄存器
v2 p0 第一个参数寄存器
v3 p1 第二个参数寄存器
v4 p2 第三个参数寄存器

Motivation for introducing parameter registers(引入参数寄存器的原因)

p开头命名方案的引入是为了解决大家在编辑smali过程中的共同烦恼。比方说,你想添加一下代码到一个有几个参数的方法,然后你发现,你需要一个额外的寄存器。你可能会想“这不是小问题嘛,只需要在.registers指令上加一个不就完事了”。

显然,问题不可能那么简单。记住一点,方法参数是存放在可用寄存器中倒数的几个寄存器中。如果你增加寄存器的数量,你将会改变方法参数存放的寄存器。因此,你必须更改.registers指令并重新编号每个参数寄存器。

但是如果在整个方法中使用p命名方案引用参数寄存器,则可以轻松更改方法中的寄存器数量,而无需担心重新编号任何现有寄存器。

注意:默认情况下,baksmali将使用参数寄存器的p命名方案。如果由于某种原因要禁用此命令并强制baksmali始终使用v命名方案,则可以使用-p / - no-parameter-registers选项。

Long/Double值

如前所述,Long和Double类型(分别为J和D)是64位值,需要2个寄存器。在引用方法参数时要记住这一点。
比如,有一个非static方法

1
LMyObject;->MyMethod(IJZ)V

方法参数类型依次是:LMyObject;, int, long, bool。所以,这个方法需要5个寄存器来存放参数。

Register Type
p0 this
p1 I
p2,p3 J
p4 Z

SmaliBaksmali2.2

v2.2版本后,smail和baksmali有了新的脚手架工具。

1
2
baksmali disassemble app.apk -o app
smali assemble app -o classes.dex

执行baksmali helpsmali help 查看入门内容。

注意以下几点:

  • 你可以把apk或者oat文件当成文件夹,单独指定其中的文件作为对象,就像这样 baksmali disassemble app.apk/classes2.dex。执行baksmali help input查阅更多相关信息。
  • deodexing时指定bootclasspath/classpath的命令选项做了小的调整。现在deodexing oat 文件,一般这样操作adb pull /system/framework framework && baksmali deodex app.odex -b framework/arm/boot.oat
  • baksmail现在包含一系列列举dex/oat/apk 文件各种相关信息的命令
    • baksmali list dex boot.oat列举出oat文件中所有dex(此命令也适用于apk文件)
    • baksmali list classes app.apk列举apk/dex等文件中的所有类
    • baksmali list methods app.apk | wc -l获取当前dex文件中的方法数
  • V2.2支持在Nougat版本上解码oat文件,并包含一些针对Marshmallow版本的错误修正
  • 在dexlib2等文件中有一些重大的API更改。因此,如果你打算将工具升级到dexlib2 v2.2,你可能需要花点事件去解决一些问题

SmaliBaksmali2.0

smali/baksmali 2.0版本是下一个重大更新版本,主要就是dexlib的更新。当前正处于测试阶段,如果想体验一下,可以自行前往下载界面下载2.0b1 jar文件。

这对你意味着什么?大部分内容对你没有影响,值得一提的是它运行更快并且使用更少的内存。

但是,下面这几点你还是需要稍微注意一下。

Multithreading(多线程)

smali和baksmali现在是多线程的!默认情况下,它们使用可用的CPU数,最多为6。你可以使用-j选项进行设置。
为了在具有多线程的smali中获得最佳性能,最好稍微提高内存。如果你正在使用smali wrapper script,则可以通过添加-JXmx512m选项手动执行此操作。

1
smali -JXmx512m out -o classes.dex

当然,你可以使用默认只位512m的smali wrapper script

Language Changes(语言改动)

我利用一次重大修订的机会来调整语言本身的一些内容。

.parameter -> .param

1
2
3
4
5
.method parameters(IILjava/lang/String;)V
.parameter
.parameter
.parameter stringParameter
...

变成

1
2
3
.method parameters(IILjava/lang/String;)V
.param p3, "stringParameter"
...

.parameter指令现在为.param,它的工作方式与.local指令类似。 .param指令不是按顺序将.parameter信息与每个参数匹配,而是使用register参数指定与之关联的参数。

.array-data changes

1
2
3
4
5
6
.array-data 0x4
0x0t 0x0t 0x80t 0x0t
0x0t 0x0t 0x40t 0x0t
0x0t 0x0t 0x20t 0x0t
0x0t 0x0t 0x10t 0x0t
.end array-data

变成

1
2
3
4
5
6
.array-data 4
0x800000
0x400000
0x200000
0x100000
.end array-data

.array-data中的每个数字现在都是它自己的元素,而不是之前的语法,它会连接每个数字的little-endian编码,然后将完整的连接字节数组重新解释为n-byte little-endian序编码元素。

const/high16 and const-wide/high16

1
2
const/high16 v0, 0x1234
const-wide/high16 v0, 0x1234

变成

1
2
const/high16 v0, 0x12340000
const-wide/high16 v0, 0x1234000000000000

更改了* / high16指令的语法,以便它使用加载到寄存器中的实际值,而不仅仅是存储在指令中的16位。

也就是说指定除了顶部16位以外的位为非0是错误的。例如const/high16 v0, 0x12340001将会是错误的。

smalidea

smalidea是一个针对Intellij IDEA以及Android Studio 的smali语言插件。

Download(下载)

当前为alpha版本,持续开发中。

Features(特性)

当前能力

  • 语法高亮/语法错误
  • 字节码级调试
    • 断点
    • 指令级单步调试
    • 给任意寄存器添加watcher
    • 调试过程中,完全java风格表达式本地窗口支持等
  • 跳转到定义处
  • 查找引用
  • 重命名
  • 在java代码中引用smali类(但是当前还不能真正被编译)
  • 问题报告 - 从错误对话框轻松创建新的github问题

计划能力

  • 自动补全(指令名称,类名/方法名/变量名等等)
  • 纯Smali工程编译
  • 健壮性错误检测(如,全字节码检测)
  • 流畅地项目导入过程
    • 自动检测源目录
    • 选择SDK
  • APK作为新项目导入引导
  • 添加“Smali Class”到“New…”菜单中
  • 在“locals”窗格中显示具有值的所有寄存器
  • “watch”窗格中设置寄存器的值

有潜力功能

  • smali+java混合工程编译
  • “引入新寄存器”
  • 导入或者deodex设备framework作为新模块或者作为sdk
  • 公开寄存器类型分析数据
    • 随时显示寄存器的预期类型
    • 寻找寄存器设值位置

安装

  1. 下载最新smalidea zip 文件
  2. 在IDEA或者Android Studio中“Settings->Plugins”中”Install plugin from disk”
  3. “Apply”并且重启

调试

注意:单指令单步调试在 IDEA 14.1及以上版本支持(Android Studio是基于IDEA的,所以类似)。而此之前的版本,执行单步调试的时候,回调转到下一个.line指令,而不是下一条指令。

  1. 使用baksmali反编译一个应用到”src”文件夹,并将其作为一个新工程的源文件夹。baksmali d myapp.apk -o ~/projects/myapp/src
  2. IDEA中,导入新项目,选择刚才的新工程目录~/projects/myapp
  3. 在导入项目时使用“Create project from existing sources”选项
  4. 项目导入完成后,右键src文件夹,”Mark Directory As->Sources Root”
  5. 打开项目设置界面,选择或者创建合适的JDK
  6. 在设备上安装启动此应用
  7. 运行DDMS,找到对应的应用进程
  8. 在IDEA中,创建一个新的“Remote”调试配置(Run->Edit Configurations),并改变调试端口至8700
  9. Run -> Debug
  10. 当断点被触发时,应用程序将会暂停,此时你可以单步,添加watch等

或者 在Android Studio3.2下进行如下操作:

  1. 使用baksmali反编译一个应用到”src”文件夹,并将其作为一个新工程的源文件夹。baksmali d myapp.apk -o ~/projects/myapp/src
  2. 在Android Studio中,关闭当前项目,执行“Open an existing Android Studio project”
  3. 项目创建完成后,右键src文件夹,”Mark Directory As->Sources Root”
  4. 确保应用AndroidManifest.xml中“android:debuggable=”true”。打开“USB调试”并且在“开发者选项”中使用“选择待调试应用”
  5. 启动应用程序并将JDWP服务转发到localhost,adb forward tcp:8700 jdwp:$(timeout 0.5 adb jdwp | tail -n 1)
  6. 在Android Studio中,创建一个新的“Remote”调试配置(Run->Edit Configurations),并改变调试端口至8700
  7. 在Android Studio中, Run -> Debug
  8. 当断点被触发时,应用程序将会暂停,此时你可以单步,添加watch等

TypesMethodsAndField(类型,方法和字段)

类型

dalvik字节码有两个主要的类型,基本类型和引用类型。引用对象指的是对象,数组等基础类型外的所有类型。
每个基本类型都由单个字母代表。这并不是我想出来的点子,他们真真实实地以字符串的形式存储在dex文件中。它们在 dex-format.html 文档中被明确的指出来过(Android源码仓中dalvik/docs/dex-format.html)。

V void - 只能用于返回类型
Z boolean
B byte
S short
C char
I int
J long(64bits)
F float
D double(64bits)

Object则是以Lpackage/name/ObjectName;的形式出现,L表明这是一个对象类型,package/name/指代包名,ObjectName为当前对象类型名称,:作为对象的结尾。这等价于java的package.name.ObjectName,以String这个具体的类型为例子,Ljava/lang/String;等价于java.lang.String

Arrays(数字)则是以[I的形式出现,这指代的是一个int类型一维数组,也就是java中int[]。至于多维数组,只需要在前面添加一个或者多个[字符。[[I=int[][][[[I=int[][][](注意,数字的最大维度为255)。

对象数据,如[Ljava/lang/String;道理和上面是一样的。

方法

方法总是以非常详细的形式指定,包括方法的类型,方法名称,参数类型和返回类型。虚拟机需要所有这些信息才能找到正确的方法,并能够对字节码执行静态分析(用于验证/优化目的)。
一般形式如下:

1
Lpackage/name/ObjectName;->MethodName(III)Z

上面这个示例中,Lpackage/name/ObjectName;表示类型,MethodName表示方法名,(III)ZIII表示参数类型(3个int型),Z表示方法的返回类型(bool类型)。
方法参数一个接一个地列出,它们之间没有分隔符。
下面有一个更复杂的示例:

1
method(I[[IILjava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;

等价于

1
String method(int, int[][], int, String, Object[])

字段

字段同样总是以详细形式指定,包括包含字段的类型,字段名称和字段类型。同样,这些信息也是为了帮助虚拟机能够找到正确的字段,以及对字节码执行静态分析。
一般形式如下:

1
Lpackage/name/ObjectName;->FieldName:Ljava/lang/String;

这应该是不言自明的 - 它们分别是包名,字段名和字段类型。

UnresolvableOdexInstruction(暂时无法解决的Odex指令)

说明

在deodexing过程中,我们使用baksmali产生的一些smali文件中,你可能会注意到baksmali用一个throw或其他东西替换odexed指令的一些情况,其中的注释如“Replaced unresolvable optimized instruction with a throw”。

详情

比如,下面这段java代码:

1
2
Object blah = null;
blah.toString();

相对应的Smali代码为:

1
2
const v0, 0
invoke-virtual {v0}, Ljava/lang/Object;->toString();

这当然会导致空指针异常 - 但它是有效的代码。在实践中,这些案例更加隐蔽。

当代码被odexed时,invoke-virtual指令将被一个invoke-virtual-quick指令替换,如下所示:

1
2
const v0, 0
invoke-virtual-quick {v0), vtable@7

其中vtable @ 7是指向Object是抽象方法表中toString()方法的索引。

但请注意,上面的代码并没有提到该方法所在的类。由于v0始终为null,并且我们只有vtable索引,因此baksmali不可能知道要查看哪个类。所以,不可能deodex 这条指令。但是,我们可以尽量对其进行处理:用具有完全相同效果的内容来替换这条指令。记住v0始终为null,所以通过它调用任何方法都会导致空指针。所以Samli就使用同样会抛出空指针的内容来替换这条无法解决的odex指令。

除此之外,紧随上面这些无法解决的odex指令的code,由于永远无法执行而成为了冗余代码(除非有其他分支可以访问到它们)。或者在一些其他情况下,代码依赖的是一个我们无法解决的odex指令方法结果,那么这部分代码也是不可能被deodex的,因为,这部分代码无法被访问到,全被移除或者注释掉了。

因此,简而言之,这些情况不应影响字节码的语义/功能。当你看到“replaced unresolvable optimized instruction”,不必大惊小怪。Baksmali对这部分呢日用的处理不影响代码功能/语义(如果对功能/语义造成了影响,那就是baksmali的bug)。

目前,存在与此相关的已知问题,其中如果try块中的所有代码都被注释掉,因为它依赖一个无法解析的odex指令,则(空)try块保留在其中,并且当安装到设备上时,空 try块会导致dalvik拒绝dex文件。

# Smali
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×